#include <getopt.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/inotify.h>

//#define DEBUG
#define MYNAME "autoext"

char *usage = "autoext -dho\n"
	"-d --daemon     run as daemon\n"
	"-h --help       print this help message\n"
	"-o --override   override already exist dir";

#define LOG(isdaemon, out, fmt, args...) do {\
	if (isdaemon) {\
		syslog(out, fmt, ##args);\
	} else {\
		fprintf(stderr, fmt, ##args);\
	}\
} while (0)

char *trim(char *s)
{
	char *p = s;
	char *curr_pos = s;
	while (*p) {
		if (*p != ' ' && *p != '\n') {
			*curr_pos++ = *p;
		}
		p++;
	}
	*curr_pos = 0;
	return s;
}

int strendwith(const char *str, const char *suff)
{
	int len1 = strlen(str);
	int len2 = strlen(suff);

	if (len1 < len2) {
		return 0;
	}
	const char *p1 = str + len1 - len2;
	int i;
	for (i=0; i<len2; i++) {
		if (*(p1+i) != *(suff+i)) {
			return 0;
		}
	}
	return 1;
}

int lockfile(int fd)
{
	struct flock fl;
	fl.l_type = F_WRLCK;
	fl.l_start = 0;
	fl.l_whence = SEEK_SET;
	fl.l_len = 0;
	return fcntl(fd, F_SETLK, &fl);
}
int daemonize(const char *cmd)
{
	// step 1
	umask(0);

	// for step 5
	struct rlimit rl;
	if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
		fprintf(stderr, "daemonize getrlimit error.\n");
		return -1;
	}

	// step 2
	pid_t pid = fork();
	if (pid < 0) {
		fprintf(stderr, "daemonize fork error.\n");
		return -1;
	} else if (pid > 0) {
		exit(1);
	}

	// step 3
	setsid();

	// step a2
	struct sigaction sa;
	sa.sa_handler = SIG_IGN;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	if (sigaction(SIGHUP, &sa, NULL) < 0) {
		fprintf(stderr, "daemonize sigaction error.\n");
		return -1;
	}
	pid = fork();
	if (pid < 0) {
		fprintf(stderr, "daemonize fork 2 error.\n");
		return -1;
	} else if (pid > 0) {
		exit(1);
	}


	// step 4
	if (chdir("/") < 0) {
		fprintf(stderr, "daemonize chdir error.\n");
		return -1;
	}

	// step 5
	int i;
	int maxof = rl.rlim_max;
	if (maxof == RLIM_INFINITY) {
		maxof = 1024;
	}
	for (i=0; i<maxof; i++) {
		close(i);
	}

	// step 6
	int fd0, fd1, fd2;
	fd0 = open("/dev/null", O_RDWR);
	fd1 = dup(fd0);
	fd2 = dup(fd0);

	openlog(cmd, LOG_CONS, LOG_DAEMON);
	if (fd0!=0 || fd1!=1 || fd2!=2) {
		syslog(LOG_ERR, "daemonize set fd0-2 error.\n");
		return -1;
	}

	return 0;
}

#define LOCK_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

int already_running(char *lock_file)
{
	int fd = open(lock_file, O_RDWR|O_CREAT, LOCK_MODE);
	if (fd < 0) {
		return -1;
	}
	if (lockfile(fd) < 0) {
		if (errno==EAGAIN || errno==EACCES) {
			close(fd);
			return 1;
		}
		return -1;
	}
	char buf[16] = {0};
	sprintf(buf, "%ld", (long)getpid);
	ftruncate(fd, 0);
	write(fd, buf, strlen(buf)+1);
	return 0;
}

static int daemonized;

static int is_daemon;
static int is_override;
void parse_options(int argc, char *argv[])
{
	struct option longopts[] = {
		{"daemon", no_argument, 0, 'd'},
		{"override", no_argument, 0, 'o'},
		{"help", no_argument, 0, 'h'},
		{0, 0, 0, 0}
	};
	int opt;
	int longidx;

	opterr = 0;
	while ((opt=getopt_long(argc, argv, "do", longopts, &longidx)) > 0) {
		switch (opt) {
		case 'd':
			is_daemon = 1;
			break;
		case 'o':
			is_override = 1;
			break;
		case 'h':
			printf("%s\n", usage);
			exit(0);
			break;
		default:
			fprintf(stderr, "Usage: %s [-do] [--daemon][--override]\n", MYNAME);
			exit(-1);
		}
	}
#ifdef DEBUG
	printf("is_dameon: %d\n", is_daemon);
	printf("is_override: %d\n", is_override);
#endif //DEBUG
}

#define WD_MAX 64
struct watched_dir_t {
	char *path;
	int wfd;
};
static struct watched_dir_t watched_dirs[WD_MAX];
static int wd_num;

#ifdef DEBUG
#define CONFIG_FILE "/home/hu/proj/autoext/autoext.conf"
#define LOCK_FILE "/home/hu/proj/autoext/autoext.pid"
#else //DEBUG
#define CONFIG_FILE "/etc/autoext.conf"
#define LOCK_FILE "/var/run/autoext.pid"
#endif //DEBUG

#define DIR_MAX 256

void read_config()
{
	FILE *fp = fopen(CONFIG_FILE, "r");
	if (!fp) {
		syslog(LOG_ERR, "Cannot open config file.\n");
		exit(-1);
	}

	char line_buf[DIR_MAX];
	char *key, *value;
	while (fgets(line_buf, DIR_MAX, fp)) {
		key = trim(line_buf);
		value = strchr(key, '=');
		if (!value) {
			syslog(LOG_INFO, "read config error, expected =\n");
			continue;
		}
		*value = 0;
		value++;
		if (!strcmp(key, "entry")) {
			watched_dirs[wd_num].path = malloc(DIR_MAX);
			strncpy(watched_dirs[wd_num].path, value, DIR_MAX);
			wd_num++;
		}
#ifdef DEBUG
		syslog(LOG_INFO, "new entry: %s\n", value);
#endif //DEBUG
	}
}

#define ARGS_MAX 4
struct tar_t {
	char *suffix;
	char *args[ARGS_MAX];
};
#define SUPPORTED 3
struct tar_t tars[SUPPORTED] = {
	{".tar", {"tar", "-xf", NULL, 0}},
	{".tar.gz", {"tar", "-xzf", NULL, 0}},
	{".tar.bz2", {"tar", "-xjf", NULL, 0}}
};
int istar(const char *file)
{
	int i;
	for (i=0; i<SUPPORTED; i++) {
		if (strendwith(file, tars[i].suffix)) {
			return 1;
		}
	}
	return 0;
}
char **getexecargs(const char *file)
{
	int i;

	for (i=0; i<SUPPORTED; i++) {
		if (strendwith(file, tars[i].suffix)) {
			tars[i].args[2] = (char *)file;
			return (char **)tars[i].args;
		}
	}
	return NULL;
}
void autoext(const char *path, const char *file)
{
#ifdef DEBUG
	LOG(daemonized, LOG_INFO, "extract %s/%s\n", path, file);
#endif //DEBUG
	pid_t pid = fork();
	if (pid < 0) {
		LOG(daemonized, LOG_ERR, "extract fork error, exit.\n");
		exit(-1);
	} else if (pid == 0) {
		struct stat st;
		if (stat(path, &st) < 0) {
			LOG(daemonized, LOG_ERR, "extract stat error, exit.\n");
			exit(-1);
		}
		if (setgid(st.st_gid) < 0) {
			LOG(daemonized, LOG_ERR, "extract setgid error, exit.\n");
			exit(-1);
		}
		if (setuid(st.st_uid) < 0) {
			LOG(daemonized, LOG_ERR, "extract setuid error, exit.\n");
			exit(-1);
		}
		if (chdir(path) < 0) {
			LOG(daemonized, LOG_ERR, "extract chdir error, exit.\n");
			exit(-1);
		}
		char **args = getexecargs(file);
#ifdef DEBUG
		LOG(daemonized, LOG_INFO, "extract %s %s %s %s\n", args[0], args[1], args[2], args[3]);
#endif //DEBUG
		execvp(*args, args);
		// Should not be here.
		LOG(daemonized, LOG_ERR, "extract execvp error, exit.\n");
		exit(-1);
	}
}

char *get_watched_dir(int wd)
{
	int i;
	for (i=0; i<wd_num; i++) {
		if (wd == watched_dirs[i].wfd) {
			return watched_dirs[i].path;
		}
	}
	return NULL;
}
#define EVENTS_MAX 256
void watch_dirs()
{
	int ifd = inotify_init();

	if (ifd < 0) {
		LOG(daemonized, LOG_ERR, "inotify_init error, exit.\n");
		exit(-1);
	}

	int i;
	int wd;
	int watch_mask = IN_MOVED_TO;
	for (i=0; i<wd_num; i++) {
		wd = inotify_add_watch(ifd, watched_dirs[i].path, watch_mask);
		if (wd < 0) {
			LOG(daemonized, LOG_ERR, "inotify_add_watch error, exit.\n");
			exit(-1);
		}
		watched_dirs[i].wfd = wd;
	}

	struct inotify_event events[EVENTS_MAX];
	int size;
	while ((size=read(ifd, events, sizeof(struct inotify_event)*EVENTS_MAX))) {
		if (size < 0) {
			if (errno == EINTR) {
				continue;
			} else {
				break;
			}
		}
		struct inotify_event *ev = events;
		while ((char *)ev < (char *)events+size) {
			char *path = get_watched_dir(ev->wd);
			if (!path) {
				LOG(daemonized, LOG_ERR, "unexpected path event, ignore.\n");
				continue;
			}
			char *file = ev->name;
			if (istar(file)) {
				autoext(path, file);
			}
			ev = (struct inotify_event *) ((char *)ev + sizeof(struct inotify_event) + ev->len);
		}
	}

	// Never should be here.
	LOG(daemonized, LOG_ERR, "read inotify event error, exit.\n");
	exit(-1);
}

void clearup()
{
	int i;
	for (i=0; i<wd_num; i++) {
		if (watched_dirs[i].path) {
			free(watched_dirs[i].path);
		}
	}
}

void sigchld(int signum) {
	if (wait(0) < 0) {
		LOG(daemonized, LOG_ERR, "wait error, exit.");
		exit(-1);
	}
}
int main(int argc, char *argv[])
{
	if (atexit(clearup) < 0) {
		fprintf(stderr, "Cannot register clearup function, exit.\n");
		exit(-1);
	}
	parse_options(argc, argv);

	if (is_daemon) {
		if (daemonize(MYNAME)) {
			fprintf(stderr, "Can not run as deamon, exit.\n");
			exit(-1);
		}
		daemonized = 1;

		syslog(LOG_INFO, "autoext run as daemon.\n");
	}
	if (already_running(LOCK_FILE)) {
		LOG(daemonized, LOG_ERR, "already running, exit.\n");
		exit(-1);
	}

	struct sigaction sa;
	sa.sa_handler = sigchld;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
	if (sigaction(SIGCHLD, &sa, NULL) < 0) {
		LOG(daemonized, LOG_ERR, "sigaction for SIGCHLD error, exit.\n");
		exit(-1);
	}

	read_config();
	
	watch_dirs();
	return 0;
}
